# Copyright (c) 2021 Presto Labs Pte. Ltd.
# Author: xguo

import collections
import datetime
import dateutil
import json
import os
import pathlib
import random
import string
import sys
import tempfile
import toml
import functools


USE_CONFIG_SERVICE = False
LOG_PRIVATE_MESSAGE = False
STORE_PARSED_MESSAGE = False


if __name__ == "__main__":
  try:
    ctrl_flag = int(sys.argv[3])
  except Exception:
    ctrl_flag = 0

  ctrl_flag = '{0:08b}'.format(ctrl_flag)
  ctrl_flag = ctrl_flag[::-1]

  if ctrl_flag[0] == '1':
    LOG_PRIVATE_MESSAGE = True

  if ctrl_flag[1] == '1':
    USE_CONFIG_SERVICE = True


class CoinInfo(object):
  def __init__(self):
    from pycoingecko import CoinGeckoAPI
    self.client = CoinGeckoAPI()
    self.coin_info = self.client.get_coins_list()
    # print(json.dumps(self.coin_info, indent=2))

  def print_coin_info(self):
    print(json.dumps(self.coin_info, indent=2))

  def get_coin_id(self, currency):
    if currency.upper() in ("BCHN", "BCHABC"):
      currency = "BCH"
    elif currency.upper() == "BCHSV":
      currency = "BSV"

    for each in self.coin_info:
      if each['symbol'].upper() == currency:
        return each['id']

    raise Exception(currency)

  def get_price(self, currency, quote='USD'):
    coin_id = self.get_coin_id(currency)
    price_info = self.client.get_price(
        ids=coin_id, vs_currencies=quote.lower())
    return price_info[coin_id][quote.lower()]


@functools.lru_cache
def get_coin_price(currency):
  ci = CoinInfo()
  return ci.get_price(currency)


def get_lot_size(capital, stack, price):
  lot_size = capital / stack / price
  f = 1

  if lot_size > 1:
   while not (1 <= lot_size <= 10):
     lot_size /= 10
     f *= 10
  else:
   while not (1 <= lot_size <= 10):
     lot_size *= 10
     f /= 10
  return round(lot_size, 1) * f


def get_git_root():
  curr_dir = pathlib.Path(__file__).parent
  for _ in range(10):
    if curr_dir.joinpath('.git').is_dir():
      return curr_dir
    else:
      curr_dir = curr_dir.parent


def get_pos_range(uniq_product, lot_size, stack):
  market_type = get_mea(uniq_product).split('.')[0]
  if market_type == 'Futures':
    return -stack / 2 * lot_size, stack / 2 * lot_size
  else:
    return 0, stack * lot_size


def remove_duplicate(l):
  return [e for i, e in enumerate(l) if e not in l[:i]]


def to_timestamp(date_str):
  return int(dateutil.parser.parse(date_str).timestamp() * 1e9)


def random_string(size):
  population = string.ascii_letters + string.digits
  res = random.choices(population, k=size)
  return ''.join(res)


def get_mea(uniq_product):
  market, exchange, product = uniq_product.split(':', 2)
  me = f'{market}.{exchange}'

  if me == 'Spot.Binance':
    return me + '.v1'

  if me == 'Futures.Binance':
    if 'USDT' in product:
      return me + '.v1'
    else:
      return me + '.v1-delivery'

  if me == 'Futures.Prex':
    return me + '.v1'

  if me == 'Futures.Bitmex':
    return me + '.v1'

  if me == 'Spot.Bithumb':
    return me + '.v2'

  if me == 'Spot.Upbit':
    return me + '.v1'

  if me == 'Spot.Coinone':
    return me + '.v2'

  if me == 'Spot.Huobi':
    return me + '.v1'

  if me == 'Spot.Gdax':
    return me + '.v1'

  if me == 'Futures.Huobi':
    if 'PERPETUAL' in product:
      return me + '.v1-swap'
    else:
      return me + '.v1'

  if me == 'Spot.Okex':
    return me + '.v3'

  if me == 'Futures.Okex':
    if 'PERPETUAL' in product:
      return me + '.v5-swap'
    else:
      return me + '.v5'

  if me == 'Spot.Kraken':
    return me + '.v1'

  if me == 'Spot.Lmaxdigital':
    return me + '.v1'

  if me == 'Spot.Bitbank':
    return me + '.v1'

  if me == 'Spot.Bitflyer':
    return me + '.v1'

  if me == 'Futures.Bitflyer':
    return me + '.v1'

  if me == 'Spot.Bitstamp':
    return me + '.v2'

  if me == 'Futures.Deribit':
    return me + '.v2'

  if me == 'Options.Deribit':
    return me + '.v2'

  if me == 'Futures.Dydx':
    return me + '.v3'

  if me == 'Futures.Mexc':
    return me + '.v1'

  if me == 'Spot.Mexc':
    return me + '.v1'

  # If not found, raise exception.
  raise(Exception('Unknown product: ' + uniq_product))


def get_norm_product(uniq_product):
  return uniq_product.split(':', 2)[2]


def is_futures(uniq_product):
  market = uniq_product.split(':')[0]
  if market == 'Futures':
    return True
  else:
    return False


def get_recipe(mea_str):
  if mea_str == 'Spot.Lmaxdigital.v1':
    return 'realtime'

  config = toml.load(open('data/coin2/feed/subscribers.toml'))
  mea_str = mea_str.replace('.', '_').lower()

  # handle okex v3 and v5 feed
  if 'okex' in mea_str and 'v5' in mea_str:
    mea_str = mea_str.replace('v5', 'v3')

  config = config[mea_str]
  if 'kucoin' in mea_str:
    return 'realtime'
  else:
    return config['default_recipe']


def get_prefix(params):
  ignore_list = [
      'price_push_bp',
      'price_pull_bp',
      'force_fire_cooldown_sec',
      'sticky_bp',
      'thrift_ignore_bp',
      'thrift_ignore_levels',
      'use_agg',
      'ohlc_window_size_sec',
      'ohlc_max_len',
  ]
  tparams = collections.defaultdict(list)
  for key, val in params.items():
    if val is None:
      continue

    if key in ignore_list:
      continue

    if isinstance(val, bool):
      val = int(val)

    if isinstance(val, float):
      val = str(round(val, 9))

    if key == 'config_file':
      val = val.replace('/', '_').replace('.', '_')

    key = ''.join([e[0] for e in key.split('_')])
    tparams[key].append(val)

  str_list = []
  for key in sorted(tparams.keys()):
    val = tparams[key]
    assert isinstance(val, list)
    assert len(val) > 0
    if len(val) == 1:
      str_list.append(key)
      str_list.append(str(val[0]))
    else:
      for n, v in enumerate(val):
        str_list.append(key + str(n))
        str_list.append(str(v))
  return '_'.join(str_list)


class StratConfig(object):
  def __init__(
      self,
      *,
      exchange=None,
      tag=None,
      is_sim=False,
      config_path=None,
      interval=None,
      sim_dir=None):
    self.is_sim = is_sim
    self.sim_dir = sim_dir
    self.interval = interval

    # converter
    self.app_config = {
        'feed': {'exchanges': {}},
        'order': {'exchanges': {}},
        'log': None,
        'strategy': None,
    }

    if self.is_sim:
      config_path = pathlib.Path(config_path)
    else:
      config_path = get_git_root().joinpath(
          'python', 'coin_deploy', 'umm', 'config',
          exchange, f'umm_config_{tag}.json')

    self.config = json.loads(config_path.read_text())
    self.config_path = config_path.as_posix()

  @property
  def feed(self):
    return self.app_config['feed']

  @property
  def order(self):
    return self.app_config['order']

  def init_log(self):
    self.app_config["log"] = {
        "health_report_period_ns": 30000000000,
    }

  def init_pricer_name(self):
    for config in self.config['subconfig'].values():
      config['pricer_config']['name'] = config['symbols'][0]

  def init_strategy(self):
    config = self.config
    strategy_name = config['strategy_name']
    strategy_group = config['strategy_group']
    self.app_config["strategy"] = {
        "strategy_group": strategy_group,
        "strategy_name": strategy_name,
    }

    if USE_CONFIG_SERVICE:
        self.app_config["strategy"]["config_service_address"] = "10.21.104.34:49402"

  def init_feed(self, mea):
    if 'common' not in self.feed:
      config = {}
      if self.is_sim:
        config["type"] = "FEED_SYSTEM_TYPE_ARCHIVE"
        arch_config = {
            'archive_type': 'FASTFEED',
            'machine': 'feed-05.ap-northeast-1.aws',
            'interval': {
                'start': self.interval[0],
                'end': self.interval[1],
            },
            'worker': '0',
        }
        config['archive'] = arch_config
      else:
        config["type"] = "FEED_SYSTEM_TYPE_LIVE"
        config["websocket"] = {"num_workers": 1}
      self.feed['common'] = config

    if mea in self.feed["exchanges"]:
      return

    config = {}
    config["mea"] = mea
    config["products"] = {"norms": []}
    config["recipe"] = get_recipe(mea)
    self.feed["exchanges"][mea] = config

  def gen_sim_config_account(self, uniq_product):
    min_pos, max_pos = self.get_pos_range(uniq_product)
    reserve_pos = (max_pos + min_pos) / 2.

    nprod = get_norm_product(uniq_product)
    start_time = to_timestamp(self.interval[0])
    end_time = to_timestamp(self.interval[1])
    sim_res_path = os.path.join(self.sim_dir, '.'.join(self.interval + [nprod, "pb"]))
    config = {
        "sim_account_id": 0,
        "sim_result_filepath": sim_res_path,
        "relative_norm": nprod,
        "initial_position": 0,
        "reserve_position": reserve_pos,
        "is_snapshot": False,
        "latency_sec": 0.1,
        "cancel_latency_sec": 0,
        "dump_fill_only": False,
        "result_format": "RESULT_FORMAT_PROTO_FILE_DUMP",
        "start_timestamp": start_time,
        "end_timestamp": end_time,
        "use_order_latency_model": False,
        "makerfill_tolerance_bps": 0.0,
        "use_exchange_timestamp": True
    }
    return config

  def init_order(self, mea):
    exch = mea.split('.')[1]
    if exch in self.order["exchanges"]:
      return

    path = self.config['key_filepath']
    config_dir_path = pathlib.Path.home().joinpath('config')
    key_path = config_dir_path.joinpath(path).as_posix()

    config = {}
    config["mea"] = mea

    if self.is_sim:
      config["type"] = "ORDER_SYSTEM_TYPE_SIM_OG"
    else:
      config["type"] = "ORDER_SYSTEM_TYPE_LIVE_OG"

    config["products"] = {"norms": []}
    config["connection_config"] = {
        "key_filepath": key_path,
    }
    config["order_logger_config"] = {"store_parsed_message": STORE_PARSED_MESSAGE}
    # config["order_gateway_config"] = {"log_private_exchange_message": LOG_PRIVATE_MESSAGE}

    if os.getenv('PREX_SANDBOX'):
      config['connection_config'].update({
          "rest_host": "api.sandbox.prex.sh",
          "ws_host": "api.sandbox.prex.sh",
      })

    if os.getenv('PREX_DEV'):
      config['connection_config'].update({
          "rest_host": "api.dev.pc.prex.sh",
          "ws_host": "api.dev.pc.prex.sh",
      })

    if self.is_sim:
      config['sim_config2'] = {'accounts': []}

    self.order["exchanges"][exch] = config

  def init_pass_executor_config(self):
    for config in self.config['subconfig'].values():
      pricer_config = config['pricer_config']
      pass_exe_config = config['pass_executor_config']

      pass_exe_config['mea'] = config['mea']
      pass_exe_config['symbol'] = config['symbols'][0].split(':')[2]
      pass_exe_config['lot_size'] = pricer_config['lot_size']
      pass_exe_config['min_pos'] = pricer_config['min_pos']
      pass_exe_config['max_pos'] = pricer_config['max_pos']
      pass_exe_config['price_pull_bp'] = 10
      pass_exe_config['price_push_bp'] = 10
      pass_exe_config['filter_tag'] = "test_tag"

  def is_valid_key(self, key):
    valid_keys = {
        'walk_step_bp',
        'ohlc_window_size_sec',
        'ohlc_max_len',
        'rate_window_size_sec',
        'max_nofill_window_size_sec',
        'risk_threshold',
        'take_profit_bp',
        'stop_loss_bp',
        'force_fire_cooldown_sec',
        'use_agg',
    }
    return key in valid_keys

  def update_params(self, params):
    for key, value in params.items():
      if not self.is_valid_key(key):
        continue

      stack = params['stack']
      for config in self.config['subconfig'].values():
        uniq_product = config['symbols'][0]
        base = get_norm_product(uniq_product).split('-')[0]
        price = get_coin_price(base)
        lot_size = get_lot_size(5000, stack, price)
        min_pos, max_pos = get_pos_range(uniq_product, lot_size, stack)
        config['pricer_config'][key] = value
        config['pricer_config']['lot_size'] = lot_size
        config['pricer_config']['min_pos'] = min_pos
        config['pricer_config']['max_pos'] = max_pos

  def get_pos_range(self, uniq_product):
    base = get_norm_product(uniq_product).split('-')[0]
    conf = self.config['subconfig'][base]['pricer_config']
    return conf['min_pos'], conf['max_pos']

  def add_feed_product(self, uniq_product):
    mea = get_mea(uniq_product)
    if 'okex' in mea.lower() and 'v5' in mea.lower():
      mea = mea.replace('v5', 'v3')

    self.init_feed(mea)
    norm_prod = self.feed["exchanges"][mea]["products"]["norms"]
    norm_prod.append(get_norm_product(uniq_product))

  def add_order_product(self, uniq_product):
    mea = get_mea(uniq_product)
    self.init_order(mea)
    exch = mea.split('.')[1]
    norm_prod = self.order["exchanges"][exch]["products"]["norms"]
    nprod = get_norm_product(uniq_product)
    norm_prod.append(nprod)

    if self.is_sim:
      account = self.order['exchanges'][exch]['sim_config2']['accounts']
      account.append(self.gen_sim_config_account(uniq_product))

  def convert_to_app_config(self):
    for config in self.config['subconfig'].values():
      for symbol in config['symbols']:
        self.add_feed_product(symbol)

      self.add_order_product(config['symbols'][0])

  def gen_config(self):
    self.init_log()
    self.init_strategy()
    self.init_pricer_name()
    self.init_pass_executor_config()
    self.convert_to_app_config()

    dt = datetime.datetime.now().strftime('%Y%m%d%H%M%S') + '_' + random_string(6)
    app_config = pathlib.Path(tempfile.gettempdir()).joinpath(f'umm_appconfig_{dt}.json')
    app_config.write_text(json.dumps(self.app_config, indent=2))
    self.app_config_path = app_config

    strat_config = pathlib.Path(tempfile.gettempdir()).joinpath(f'umm_config_{dt}.json')
    strat_config.write_text(json.dumps(self.config, indent=2))
    self.strat_config_path = strat_config

    log_name = self.app_config['strategy']['strategy_name']
    app_relpath = "bazel-bin/cc/appcoin2/strategy/umm/umm_runner"
    self.cmd_line = f"{app_relpath} --driver={app_config} --config={strat_config} " + \
        "--logtostderr " + \
        f"2>> ${{HOME}}/data/log/{log_name}_stderr " + \
        f"1>> ${{HOME}}/data/log/{log_name}_stdout"
    template = """
while true; do
{cmd}
sleep 5
done
"""
    self.run_script = template.format(cmd=self.cmd_line)


def main(argv):
  sconfig = StratConfig(exchange=argv[1], tag=argv[2])
  sconfig.gen_config()
  print(sconfig.run_script)


if __name__ == '__main__':
  sys.exit(main(sys.argv))
